using System;
using System.Collections;
using System.IO;
using System.Threading;
using System.Xml.XPath;
using NUnit.Framework;

namespace PaulRz.Util
{
	/// <summary>
	/// Summary description for XPathCache.
	/// </summary>
	public class XPathCache : IDisposable
	{
		private XPathCache()
		{
			stdHandler = new FileSystemEventHandler(onFileChanged);
		}

		~XPathCache()
		{
			Dispose(false);
		}

		#region Singleton impl
		private static XPathCache _self = new XPathCache();

		public static XPathCache Instance
		{
			get
			{
				if (_self==null) _self = new XPathCache();
				return _self;
			}
		}
		#endregion

		// key - uri of file, value - object of XPathDocument
		private Hashtable xpathCache = Hashtable.Synchronized(new HashtableCI());

		private Timer timer = null;

		public XPathDocument GetXPathDocument(string fName)
		{
			string fullPath = FUtil.getConfigPath(fName);
		    if (xpathCache.ContainsKey(fullPath))
		    {
                lock (xpathCache.SyncRoot)
                {
                    CacheElement ce = xpathCache[fullPath] as CacheElement;

                    if (ce.hasChanged)
                    {
                        xpathCache.Remove(fullPath);
                        return loadNew(fullPath).Doc;
                    }
                    return ce.Doc;
                }
		    }
		    else
		    {
		        return loadNew(fullPath).Doc;
		    }
		}

		public void Flush()
		{
			xpathCache.Clear();
		}

		public bool CleanerEnabled
		{
			get
			{
				return (timer!=null);
			}
			set
			{
				if (value && (timer==null))
				{
					timer = new Timer(new TimerCallback(cleaner), null, 10000, 1000);
				} else 
					if (!value)
				{
					if (timer!=null)
					{
						timer.Dispose();
						timer = null;
					}
				}
			}
		}

		private FileSystemEventHandler stdHandler;

		private CacheElement loadNew(string fullPath)
		{
            CacheElement ce;
		    lock (xpathCache.SyncRoot)
            {
                ce = xpathCache[fullPath] as CacheElement;
                if (ce == null)
                {
                    ce = new CacheElement(fullPath, stdHandler);
                    xpathCache.Add(fullPath, ce);
                }
            }
			return ce;
		}

		#region Cache Utilities

		private readonly int threshold = 20;

		// Cleaner kills one least-recently used file once.
		private void cleaner(object dummy)
		{
			if (xpathCache.Count <= threshold)
			{
				timer.Change(2000, 3000);
				return;
			}

			timer.Change(1000, 1000);

			lock(xpathCache.SyncRoot)
			{
				long minWeight = 0;
				object minKey = null; // object - to avoid casting
				ArrayList removing = null;
				foreach (DictionaryEntry entry in xpathCache)
				{
					CacheElement ce = entry.Value as CacheElement;

					if (ce.hasChanged)
					{
						if (removing==null) removing = new ArrayList();
						removing.Add(entry.Key);
						continue;
					}

					if ((minWeight==0) | (minWeight > ce.Weight))
					{
						minWeight = ce.Weight;
						minKey = entry.Key;
					}
				}

				if (minKey!=null) xpathCache.Remove(minKey);

				if (removing!=null)
				{
					foreach (object o in removing)
					{
						xpathCache.Remove(o);
					}
				}
			}
		}

		private void onFileChanged(object source, FileSystemEventArgs e)
		{
			string fName = e.FullPath;
			if (xpathCache.ContainsKey(fName))
			{
				(xpathCache[fName] as CacheElement).hasChanged = true;
				if (timer==null) xpathCache.Remove(fName);
			}
		}

		#endregion

		#region CacheElement implementation

		private sealed class CacheElement : IDisposable
		{
			public CacheElement(string fName, FileSystemEventHandler fseh)
			{
				if (File.Exists(fName))
				{
					doc = new XPathDocument(fName);
					
					// getFileSize
					FileInfo fi = new FileInfo(fName);
					// each Kb adds a gap about 100 sec of life
					// 1024*1024 * 100 ns 
					size = fi.Length * 1024; 
					lastUsed = DateTime.Now.Ticks;

					fsw = new FileSystemWatcher(Path.GetDirectoryName(fName), Path.GetFileName(fName));
					fsw.Changed += fseh;
					fsw.Deleted += fseh;
					fsw.EnableRaisingEvents = true;
				} else throw new FileNotFoundException("File for caching is not found", fName);
			}

			~CacheElement()
			{
				Dispose(false);
			}

			private FileSystemWatcher fsw;
			
			private XPathDocument doc;

			public XPathDocument Doc
			{
				get
				{
					lastUsed = DateTime.Now.Ticks;
					return doc;
				}
			}
			
			public bool hasChanged = false;
			
			private long size;
			private long lastUsed;
			
			public long Weight
			{
				get
				{
					return size + lastUsed;
				}
			}
			#region IDisposable Members

			public void Dispose()
			{
				Dispose(true);
			}

			private void Dispose(bool disposing)
			{
				if (fsw!=null)
				{
					fsw.Dispose();
					fsw = null;
				}

				if (disposing)
					GC.SuppressFinalize(this);
			}

			#endregion
		}
		#endregion

		#region IDisposable Members

		protected void Dispose(bool disposing)
		{
			if (timer!=null)
			{
				timer.Dispose();
				timer = null;
			}
			if (xpathCache != null)
			{
				xpathCache.Clear();
				xpathCache = null;
			}
			if (disposing)
				GC.SuppressFinalize(this);
		}
		
		public void Dispose()
		{
			Dispose(true);
		}

		#endregion
	}
}
